home *** CD-ROM | disk | FTP | other *** search
/ ASME's Mechanical Engine…ing Toolkit 1997 December / ASME's Mechanical Engineering Toolkit 1997 December.iso / c_lang / dos-dev.lzh / DOSDEV.DOC < prev    next >
Encoding:
Text File  |  1986-03-21  |  22.3 KB  |  385 lines

  1. _Creating User-Installable Device Drivers in MS-DOS 2.0+_
  2.  
  3.                Bruce Bordner, 1985
  4.                1713 4th Avenue
  5.                Asbury Park, NJ 07712
  6.  
  7.  
  8.  
  9.   Prior to version 2.0, driver programs to support new (or non-IBM) 
  10.   peripherals required some complex and ugly programming to interface with 
  11.   DOS.  This became so much of a problem that it was fixed in the first major 
  12.   revision (2.0) by providing a "legal" way to install device drivers and 
  13.   interface with the DOS I/O functions.  This was primarily intended as a 
  14.   convenience for OEMs, but Microsoft did include a chapter (14) about the 
  15.   subject in the DOS manual.  I haven't found any better source on the 
  16.   subject; which means that I found practically no other information.  [Only 
  17.   other source: "Modifying MS-DOS Device Drivers" by Mike Higgins, Computer 
  18.   Language 3/85 - a good article which can clarify the DOS documentation, but 
  19.   does not treat several points explained here.] Assuming for the moment that 
  20.   driver software can be home-brewed, it is not an obviously useful technique.  
  21.   You could make your own driver for a 500 megabyte drive rather than waiting 
  22.   for the manufacturer to do it, but this is a last-ditch move.  However, a 
  23.   driver does not necessarily have to be controlling a physical device.  A DOS 
  24.   device driver must be a COM file, which limits the code space to 64K total.  
  25.   It has a specified header and function call structure.  DOS will only make 
  26.   and accept I/O operations which are given in the manual. You cannot return 
  27.   an error code other than those used by DOS, and most of those are not 
  28.   returned to your calling program.  Other than that, you can do as you like.  
  29.   This opens up many possibilities.  Driver software becomes a part of the 
  30.   memory-resident sections of DOS during the boot-up operation.  It becomes 
  31.   another resource available to any of your programs.  The classic example, as 
  32.   given in the DOS manual, is a ram-based disk simulator.  This is a "virtual" 
  33.   device, where the code and data is seen as a package by the operating system 
  34.   and user programs.  This module can perform any function which you can fit 
  35.   into a COM program, with predefined interfaces to DOS, other programs, and 
  36.   other device drivers.  Although Microsoft built some limiting assumptions 
  37.   into DOS which make it difficult to implement certain functions, many 
  38.   possibilities remain.  One option which I would like to explore is to 
  39.   offload a driver to an outboard processor for concurrent background 
  40.   operation.  
  41.  
  42. _DOS Function Calls for Device Drivers_
  43.  
  44.   In order to keep things simple, I only used the "extended file management" 
  45.   (version 2.0) functions under system interrupt 21H. These are used by the 
  46.   "fread()" and "fwrite()" functions of the C86 C compiler.  The primary 
  47.   functions affecting drivers are: 
  48.  
  49.     3D Open a file or device, return a 16-bit file handle in register 
  50.        AX.
  51.  
  52.     3E Close the file or device associated with the handle in BX.
  53.  
  54.     3F Read from a file or device.  The following registers must be
  55.        loaded as indicated:
  56.                BX => file handle of device
  57.                CX => number of bytes to read ( 64K max)
  58.                DX => segment offset of your data buffer storage
  59.                DS => segment of your data buffer
  60.        Reads CX bytes from a device into the buffer address given.
  61.        *** What is not explained by Microsoft is that DOS actually 
  62.        requests only one byte per call to the device driver, making
  63.        CX number of calls.  This has all sorts of ugly side effects,
  64.        which will become evident in the description of my sample
  65.        driver.  Microsoft has apparently built in the assumption that
  66.        all character-oriented devices talk to fairly slow serial 
  67.        hardware, like printers.  If I read this right, DOS has the
  68.        ability to respond to interrupts between each byte transferred,
  69.        but it does make things clumsy.  DOS apparently increments the
  70.        DS:DX buffer location after each call.  The driver will be
  71.        re-run from START for each byte, and must maintain its own 
  72.        data pointers to maintain synchronism with the DOS transfer.
  73.  
  74.     40 Write to a file or device.
  75.        Same as above, but data is transferred to your buffer area.
  76.        *** Same warning.
  77.  
  78.     44 I/O Control for Devices.  This function has 8 subfunction codes
  79.        which are used for some rudimentary device controls. 
  80.        Subfunction 2 is to read CX number of bytes from the "control
  81.        channel" of the device, with the same register settings as for
  82.        the normal read.  The stated purpose is to provide a way of 
  83.        reading device driver status rather than data from the device
  84.        itself.  However, up to 64K bytes made be transferred per call.
  85.        What your device does with it is up to you.
  86.        Subfunction 3 is the corresponding write call.
  87.        *** For these calls, DOS actually requests CX bytes from the  
  88.        device on one call.  This is used in the second version of my
  89.        sample driver, which is much simpler than the standard
  90.        read-write calls used in the first version.
  91.  
  92. _How DOS Translates Your Read/Write Function Calls into Device Driver Requests_
  93.  
  94.   When any of the above functions is called by your application program, DOS 
  95.   develops a data structure called the "Request Header" by the manual.  This 
  96.   structure consists of a 13-byte defined header which may be followed by 
  97.   other data bytes depending on the function requested.  The fixed part of the 
  98.   request header is as follows: 
  99.  
  100.   _BYTE_       _PURPOSE_
  101.     0  Length in bytes of the total request header (0-255)
  102.     1  Unit code, used to determine subunit to use in block devices
  103.          (not used for character devices)
  104.     2  Command code (0-12) to activate specific device function
  105.    3-4 Status word, returned by the driver
  106.    5-12        The manual states that this area is "reserved for DOS". 
  107.        Another source indicates that this consists of two double-word
  108.        (4-byte) pointers to be used to maintain a linked list of 
  109.        request headers for this device and a list of all current 
  110.        device requests being processed by DOS.  This is apparently
  111.        in the works for a future concurrent-DOS.
  112.  
  113.   The 13 command codes are detailed on pages 14-12 of the manual; only the 
  114.   following are used by the character devices explained in this paper: 
  115.  
  116.   _CODE_       _FUNCTION_
  117.     0  INIT - perform all initialization required at DOS boot time
  118.          to install the driver and set local driver variables.
  119.     3  IOCTL INPUT - read a specified number of bytes from the 
  120.          device driver's IO control channel.
  121.     4  INPUT - normal device "read".  Reads a number of bytes from
  122.          the device your driver is controlling.
  123.     8  OUTPUT - normal device "write" call from user program.
  124.    12  IOCTL OUTPUT - write bytes to driver control channel.
  125.  
  126.   For each of these function calls, the driver receives the following:
  127.  
  128. INIT:  This function must be built into any driver program.  It is called only 
  129. by DOS during boot time, to reserve the system memory needed to hold the 
  130. driver and to link the driver into the set of active devices managed by DOS.  
  131. DOS sends:     13-byte request header 
  132.        BYTE number of units (not used by char devices)
  133.        DWORD ending address of driver
  134.        DWORD pointer to BPB array (not used by char devices)
  135. The driver program must load the ending address at a minimum; any local 
  136. initialization may also be performed at this time.  
  137.  
  138. INPUT, OUTPUT, IOCTL INPUT, or IOCTL OUTPUT:
  139.   For all of these, DOS sends:
  140.   13-byte request header
  141.   BYTE media descriptor (not used for char devices)
  142.   DWORD offset and segment of the data buffer in calling program
  143.   WORD number of bytes to transfer in this call
  144.   WORD starting sector (not used for char devices)
  145. The driver must perform the requested read or write function, set the "number 
  146. of bytes to transfer" location to the number actually done, and set the status 
  147. word in the request header to indicate any errors.  
  148.  
  149.   The actual use of these structures will be detailed in the driver function 
  150.   description.  
  151.  
  152. _Required Structure for a Device Driver_
  153.  
  154.   Listing 1 (DOSDEV.ASM) is a template containing the minimum requirements for 
  155.   a character-oriented device driver.  This is detailed in pages 14-3 to 14-8 
  156.   of the DOS manual.  The driver program must meet the requirements of a 
  157.   normal COM file.  However, COM files usually start with an ORG 100H to allow 
  158.   room for the DOS Program Segment Prefix structure.  For a driver, you must 
  159.   use ORG 0, as the PSP is not used.  The Device Header data structure must be 
  160.   the first object defined in your file.  It consists of: 
  161.   DWORD        Pointer 
  162.          to the next device driver currently installed. This 
  163.          should be initialized to -1, DOS will fill this field as
  164.          necessary during system initialization (boot).
  165.   WORD Device attribute.  I used C000H to indicate that this is a
  166.          character device with IOCTL capability.  This field is also
  167.          used to indicate if this device is to be the standard output
  168.          or input device.
  169.   WORD Pointer to "device strategy" function in the driver.  This 
  170.          function is called whenever a request is made to the driver,
  171.          and must store the location of the request header from DOS.
  172.   WORD Pointer to function which activates driver routines to perform
  173.          the command in the current request header.  This is called by
  174.          DOS after the call to the strategy function, and should reset
  175.          to the request header address stored by "strategy", to allow
  176.          for the possibility of interrupts between the two calls.
  177.   8-BYTES Name field.  For character devices, fill this with the name
  178.          which you must use when opening the device.
  179.  
  180.   After this structure, you may include any local data definitions needed for 
  181.   the internal operation of your driver.  The DOSDEV example includes only the 
  182.   minimum; a pointer to the request header and a table of addresses of the 
  183.   functions which will be called by the command code from DOS. The function 
  184.   addresses are arranged according to their calling function code (0 to 12) so 
  185.   that the function router can use the DOS command code as an offset into this 
  186.   table.  
  187.  
  188. _Required Device Driver Functions_
  189.  
  190.   For simplicity, I will discuss these functions as given in the DOSDEV.ASM 
  191.   listing.  
  192.  
  193.   XDV STRAT:  This function is called directly by DOS when a request has been 
  194. made to use this device.  Its only purpose is to save a segment and offset 
  195. pointer to the request header.  At the time DOS calls the device, the segment 
  196. of the request header is in register ES and the offset is in register BX. 
  197. These values are copied into the variables RH SEG and RH OFF.  The fact that 
  198. Microsoft calls this a "device strategy" function leads me to believe that 
  199. more complex processing will be required in this function when DOS becomes 
  200. multi-user or multi-processing oriented.  
  201.  
  202.   XDV FUNC:  This is called by DOS immediately after XDV STRAT.  The function 
  203.   pushes all machine registers to save the current data until the device has 
  204.   finished the requested operation.  Data segment register DS is set to the 
  205.   Code segment value, as all local variables exist in the code segment. 
  206.   Registers ES and BX are loaded from RH SEG and RH OFF to reset them to the 
  207.   start of the DOS request header.  The command code from the request header 
  208.   (at ES:[BX+2] ) is then used as an offset into the function address table 
  209.   FUNTAB to initiate the driver function requested.  In DOSDEV, only the INIT 
  210.   function has been coded, all others drop out to EXIT after setting the 
  211.   status word of the request header to "done; no error".  All you need to do 
  212.   is fill in the function code for any driver function you intend to use.  
  213.  
  214.   INIT:  When DOS is booted, it reads your CONFIG.SYS file to determine which 
  215.   programs to install as device drivers (DEVICE=filename.ext).  After loading 
  216.   the file image into memory, DOS sends a request header with the command code 
  217.   "0" to the device.  The INIT function must load an offset (at ES:[BX+14]) 
  218.   and segment value (at ES:[BX+16]) into the request header to indicate the 
  219.   ending address for the driver program, including space for any memory used 
  220.   as a virtual device.  The function may also do any initial variable setting 
  221.   within the driver.  INIT then exits back to DOS, which uses the address 
  222.   given to set the boundary of DOS including the new driver storage.  
  223.  
  224.   EXIT:  This function restores all machine registers and returns to DOS.
  225.  
  226. _Examples of Character-Oriented Device Drivers_
  227.  
  228.   Listing 1 (STKDEV.ASM) and 2 (STKDEV2.ASM) show the use of a virtual device 
  229.   driver to implement a "stack".  User programs may "push" bytes or entire 
  230.   records by writing them to the device, and "pop" them with a read request.  
  231.   I/O control calls are used to set the record size to be used by the driver.  
  232.   This may not be very useful in itself, but this example shows solutions to 
  233.   most of the problems without being difficult to read.  
  234.     STKDEV is constructed in the recommended fashion; I got much of the code 
  235.   from the example device driver in the DOS manual.  Read and write calls from 
  236.   the user program activate the functions INPUT and OUTPUT, while IOCTL IN and 
  237.   IOCTL OUT are used to read and write the record size setting.  I developed 
  238.   the first version in a few days, but then spent two months of spare time 
  239.   trying to find out why it wouldn't work.  It's an undocumented feature of 
  240.   MS-DOS, although I can see some hints of it in the manual - now that I know 
  241.   what to look for.  When your user program makes a read or write call to a 
  242.   device, you send DOS the number of bytes to transfer, which may be 1 to 64K.  
  243.   You make one call to DOS (interrupt 21H). The request header for I/O 
  244.   contains a full word to contain the byte count sent from DOS.  I made the 
  245.   mistake of assuming that when I make a 10 byte I/O request to my driver, the 
  246.   driver would see a 10 byte count.  Actually, it sees 10 unrelated 1-byte 
  247.   requests from DOS.  STKDEV's INPUT and OUTPUT functions show the effects.  I 
  248.   had to establish two new variables (NUM2READ and NUM2WRITE) to keep track of 
  249.   how many bytes had been transferred, so that the driver would know if it was 
  250.   done with a "record".  This is required because the "top of stack" pointer 
  251.   (CURRENT) is set to the next free address following the last byte written.  
  252.   A "pop" operation (INPUT) requires decrementing the pointer by the record 
  253.   size, transferring a full record in the byte order written, then resetting 
  254.   the pointer back to the used record's start to allow overwriting and 
  255.   repeated "pops".  There must be an easier way to do this, but I think this 
  256.   mess shows the problems more clearly.  
  257.     STKDEV2 uses IOCTL functions rather than the standard I/O.  On IOCTL 
  258.   calls, DOS sends the full byte count in the request header.  This made 
  259.   things simpler in my driver code, but complicated my user programs by 
  260.   requiring custom read/write functions.  Take your choice.  DOS apparently 
  261.   starts at the buffer address which your program supplies in the I/O call, 
  262.   transferring one byte with a request to the specified driver, then 
  263.   incrementing the buffer pointer by 1, and repeating until the specified 
  264.   number of bytes is copied. The device driver must track this indexing 
  265.   carefully in some applications, for others it may not matter.  
  266.  
  267.   Mike Higgins' article included many debugging tips.  One of them is the 
  268.   "yell" macro at the beginning of STKDEV.  This displays one character on the 
  269.   screen by writing directly to the video memory.  If you use DOS function 
  270.   calls to display the status of your driver, DOS will overwrite the request 
  271.   header which your driver has started processing.  I have left "yell" 
  272.   invocations throughout the function code; it was this macro that finally 
  273.   showed me what my device was receiving from DOS.  
  274.  
  275. Functional Description of STKDEV.ASM:
  276.  
  277.   The procedure and device name is XSTK, which must be used when opening the 
  278.   device for I/O.  It is assembled and linked normally, then use EXE2BIN to 
  279.   convert the EXE file to COM form. I used EXE2BIN STKDEV.EXE XSTK.SYS, 
  280.   changing the file name because any references to XSTK once it is installed 
  281.   cause weirdness.  The CONFIG.SYS file must contain DEVICE=XSTK.SYS.  Reboot 
  282.   and XSTK has added 32K+ to memory-resident DOS.  
  283.  
  284. XSTK STRAT:
  285.   This is the "device strategy" function, which is called first by DOS for 
  286.   every request header. DOS has set ES and BX to the address of the request 
  287.   header; these are stored RH SEG and RH OFF to ensure that the driver will be 
  288.   able to find the request header.  It may be omitted for DOS 2.0.  
  289.  
  290. XSTK FUNC:
  291.   DOS calls this entry point second on all device requests.  The call is a 
  292. signal to begin processing the data in the request header.  DOS is now 
  293. suspended (in this version) until your device returns to it.  All machine 
  294. registers are saved on the stack, and ES and BX are reloaded to the address 
  295. stored by XSTK STRAT.  The command code at ES:[BX+2] is used as an index to 
  296. jump to the requested function.  
  297.  
  298. INIT:
  299.   This function is called only by DOS, only during installation (boot) time.  
  300.   As it will not be needed while the device is operating, INIT could be 
  301.   located after the end address returned to DOS, saving some memory.  STORAGE 
  302.   is the variable marking the end of the XSTK code.  However, I add 32K for 
  303.   stack storage.  This value is then copied to the request header and returned 
  304.   to DOS for memory allocation.  Local variables are set to default "stack 
  305.   empty" values.  
  306.  
  307. IOCTL IN:
  308.   Used to read the current record size from the driver into the calling 
  309.   program's data buffer.  In order to use the REP MOVSB instruction, CX is set 
  310.   to the requested byte count, DS and SI point to the internal variable 
  311.   RECSIZE, ES and DI point to the buffer address contained in the request 
  312.   header.  SI and DI are incremented by the REPeat prefix until CX bytes have 
  313.   been transferred.  ES and BX are then reset to the request header address.  
  314.  
  315. IOCTL OUT:
  316.   Write a new record size to the device.       Same deal as above, backward.  
  317.  
  318. INPUT:
  319.   Processes read requests.  The double word at ES:[BX+14] contains the address 
  320.   of the data buffer in the calling program, and the word at ES:[BX+18] is the 
  321.   byte count for the request.  This value will always be 1 for DOS 2.0, but 
  322.   this is subject to change.  The first process required is to check whether 
  323.   the previous write (OUTPUT) completed storing RECSIZE bytes.  This is done 
  324.   by checking the NUM2WRITE variable.  If NUM2WRITE is not 0, the CURRENT 
  325.   pointer is set to a record boundary before reading.  Next, INPUT checks to 
  326.   see if it is in the process of reading a record or if it is starting a new 
  327.   record.  If NUM2READ is 0, INPUT must reset CURRENT to the start of the last 
  328.   record written.  At this time, INPUT checks CURRENT against BOTMEM to ensure 
  329.   that reads will not go past the bottom of the stack space.  I tried to 
  330.   return an error code of 30H, to give my calling program a different error 
  331.   than those used by DOS.  However, DOS apparently checks this value against 
  332.   the approved list, and I get a "Disk drive error" on the display.  So, it 
  333.   appears that only the given error codes will be sent to calling programs.  
  334.   The actual read transfer is set up at PULLIT.  Again, I used the REP MOVSB 
  335.   instruction, even though DOS will only call for one byte per request header.  
  336.   CX is loaded with the count from the request header, DS and SI have been set 
  337.   to the proper address in the stack storage, ES and DI are set to the data 
  338.   buffer address of the calling program. NUM2READ is decremented on each 
  339.   request.  While NUM2READ is not 0, the value of SI is stored in CURRENT; SI 
  340.   has been incremented by the REP MOVSB to point at the next byte of the 
  341.   record.  If NUM2READ is 0, a full record has been read and CURRENT must be 
  342.   reset to the starting address of the record.  Finally, ES and BX are reset 
  343.   to point to the DOS request header.  The status word of the request header 
  344.   is filled with the code for "done; no error" and the process completes 
  345.   through EXIT.  
  346.  
  347. OUTPUT:
  348.   Similar to INPUT, except that CURRENT always increments.
  349.  
  350.  
  351. Description of STKDEV2:
  352.  
  353.   This version reverses the use of IOCTL and INPUT/OUTPUT.  Most of the code 
  354.   is the same as STKDEV, but the variables NUM2READ and NUM2WRITE are no 
  355.   longer needed, as the DOS request header will request the actual number of 
  356.   bytes given by the calling program.  THerefore, the driver implicitly knows 
  357.   that each request will consist of a complete record.  If you compare IOCTL 
  358.   IN with INPUT, and IOCTL OUT with OUTPUT of STKDEV, it is obvious that this 
  359.   approach was easier to code for this application.  
  360.  
  361.  
  362. _Testing the Sample Device Drivers_
  363.  
  364.   Listing 4 (TXSTK.C) is a C program to perform simple test calls to STKDEV.  
  365.   Listing 5 (TXSTK2.C) tests STKDEV2 by using IOCTL calls in place of the 
  366.   read/write system calls used in TXSTK.  
  367.  
  368.   TXSTK uses segread() to determine the DS segment value of itself.  This is 
  369.   used to pass DOS the segment value of the data buffer "instr".  Then XSTK is 
  370.   opened.  I used sysint21() calls instead of fopen, fwrite() and fread() just 
  371.   to simplify matters.  "Outstr" is then written to XSTK.  Although I fill 
  372.   callregs.cx with the count of 5, I know that DOS will make 5 one-byte calls.  
  373.   XSTK is currently set to the default RECSIZE of one, so a write and 
  374.   corresponding read produces "olleH" from my string "Hello" written.  I then 
  375.   use IOCTL calls to reset XSTK's RECSIZE to 5 bytes.  Although the following 
  376.   write and read are in the same form as before, XSTK now knows to treat input 
  377.   as 5-byte records.  So, "Hello" returns "Hello".  
  378.  
  379.   TXSTK2 is the same through opening XSTK.  However, the first byte-at-a-time 
  380.   write/read must use a loop to cycle through the 5 characters of the output 
  381.   and input strings.  After resetting XSTK's RECSIZE to 5 using I/O calls, the 
  382.   write/read calls request 5 bytes, which is now processed in one call to 
  383.   XSTK.  The strings are returned as with TXSTK; "olleH" and "Hello".  
  384.  
  385.